package com.pktech.framework
{

	import com.pktech.flexutil.Diagnostics;
	import com.pktech.flexutil.ReflectionUtil;
	import com.pktech.flexutil.Set;
	import com.pktech.flexutil.events.MethodReturnEvent;
	import com.pktech.flexutil.gc.SharedReference;

	import flash.events.Event;
	import flash.utils.Dictionary;

	import mx.utils.StringUtil;

	/**
	 * This class is responsible for creation and/or invocation of commnand objects or
	 * methods. It maintains command maps, which are maps from request event ids to command
	 * class type and creates instances of these classes (or just invoke static methods) as
	 * request events appear on the associated event bus.
	 *
	 * <p>The simplest way to write commands is in the form of a class with one or more
	 * static methods, each of which targets a specific request type. These methods are
	 * marked with the [RequestHandler] tag and being static, they take their entire context
	 * as parameters. The request event is always the first parameter and the last parameter
	 * can optionally be a callback (which takes MethodReturnEvent as its arg). There can be
	 * any number of parameters between the first arg and the optional last arg, values for
	 * which are looked up or created as necessary by Frinje. These other parameters are
	 * usually the targets of the command task, e.g. proxy objects.
	 *
	 * <listing version="3.0">
	 * public class StaticCommands
	 * {
	 *    [RequestHandler("someRequest")]
	 *    public static function onSomeRequest( request:SomeRequestEvent,
	 *                                          target:ISomeProxy,
	 *                                          callback:Function ):void {
	 *       target.performSomeTask( request.someProp, callback );
	 *    }
	 *    // ...
	 * }
	 * </listing>
	 *
	 * <p> This class could be registered using CommandRegistry's addCommand method at app or
	 * module initialization, or could be registered through a CommandSet just like any other
	 * command class.
	 *
	 * <p>A relatively more complex command can be implemented as a class whose instances are
	 * created specially to service individual request events. Such a command class may look
	 * like the following:
	 *
	 * <listing version="3.0">
	 * public class SomeCommand
	 * {
	 *    [InjectableProxy]
	 *    public var proxy:ISomeProxy;
	 *    // ...
	 *    [InjectableRequest]
	 *    public var request:SomeRequestEvent;
	 *    // ...
	 *    [RequestHandler("someRequest")]
	 *    public function execute():void {
	 *       proxy.performSomeTask( request.whatever,
	 *                              CallbackUtil.dispatchResponseUsingRequestContext,
	 *                              request );
	 *    }
	 * }
	 * </listing>
	 *
	 * The command class above could also implement ICommand explicitly. Or, it could instead
	 * use one or more request handlers to handle the request events, as illustrated in the
	 * following example:
	 *
	 * <listing version="3.0">
	 * public class SomeCommand
	 * {
	 *    [InjectableProxy]
	 *    public var proxy:ISomeProxy;
	 *    // ...
	 *    [RequestHandler("someRequest")]
	 *    public function onSomeRequest( request:SomeRequestEvent ):void {
	 *       // ...
	 *    }
	 *    // ...
	 *    [RequestHandler("someOtherRequest")]
	 *    public function onSomeOtherRequest( request:SomeOtherRequestEvent ):void {
	 *       // ...
	 *    }
	 * }
	 * </listing>
	 *
	 * <p>The command class could also provide its own callback, as shown below, as opposed
	 * to using the one from CallbackUtil:
	 *
	 * <listing version="3.0">
	 * public function execute():void {
	 *    proxy.performSomeTask( request.whatever, onProxyReturn );
	 * }
	 * // ...
	 * private function onProxyReturn( ev:MethodReturnEvent ):void {
	 *    // ...
	 * }
	 * </listing>
	 *
	 * <p>If the command class is an <code>IEventDispatcher</code> (e.g., if it extends
	 * <code>EventDispatcher</code>), it could also raise the response event (rather than
	 * dispatching it through <code>CallbackUtil</code>).
	 *
	 * <listing version="3.0">
	 * [Event(name="someReturn", type="com.some.ns.events.SomeResponseEvent")]
	 * public class SomeCommand extends EventDispatcher
	 * {
	 *    // ...
	 *    public function execute():void {
	 *       proxy.performSomeTask( request.whatever, onProxyReturn );
	 *    }
	 *    // ...
	 *    private function onProxyReturn( ev:MethodReturnEvent ):void {
	 *       var response:SomeResponseEvent = ...;
	 *       // ...
	 *       response.request = request;
	 *       dispatchEvent( response );
	 *    }
	 * }
	 * </listing>
	 *
	 * <p>The above example is an execute style command, but it could just as well be a
	 * request-handler style command and the EventDispatcher part would still apply.
	 *
	 * <p>The proxy interface referenced in the above examples (i.e., ISomeProxy) may have
	 * multiple implementations. The only requirement for injectability purposes is that the
	 * interface should declare a [DefaultImplementation].
	 *
	 * <p>If a command object invokes other commands (through request events, as opposed to
	 * directly invoking proxy or any other methods with callbacks, for example), and the
	 * request event definition doesn't have the form (or metadata annotations) that EventBus
	 * prescribes for the request-response pattern, then the dispatching command needs to take
	 * responsibility for its own lifetime so that it would stick around while those other
	 * commands are in progress. Here's one way of accomplishing this:
	 * <ol>
	 *   <li>In the command constructor, let the frameworks (i.e., frinje, etc) know that this
	 *       object exists by raising an event through ObjectEvent.newAddition. This event can
	 *       be raised either on the command itself (if it's an EventDispatcher, which it
	 *       would be if it's to receive responses from other commands), or on the
	 *       Application.
	 *       This would make sure that the command gets registered on the event bus in a
	 *       strong sense. That would also allow the use of ResponseHandler tag in this
	 *       command class, as well as alleviate the weak-reference/lifetime issues.
	 *   <li>When the whole command execution is done, raise the object removal event using
	 *       ObjectEvent.newRemoval, so that the object gets unregistered from the framework
	 *       services.
	 * </ol>
	 *
	 * <p>The request-response pattern is usually used with the (short-lived) command objects,
	 * but it can also be used by objects of indefinite lifetime that already exist at the
	 * time of request event dispatch. No commands need to be registered in that case, and the
	 * regular [EventHandler] tag is used (rather than the [RequestHandler] tag) to mark the
	 * request handler method.
	 *
	 * @see EventBus
	 * @see ProxyRegistry
	 * @see CallbackUtil
	 * @see ICommand
	 * @see MethodReturnEvent
	 */
// XXX (nice to have): Introduce a ICommandFactory interface that this class impl's
	public class CommandRegistry
	{
		private static const CONVENTIONAL_CMD_METHOD_NAME:String="execute";
		private static var _instance:CommandRegistry;
		private var _mappings:Dictionary=new Dictionary();
		private var _injectionMgrRef:SharedReference=SharedReference.newReference(InjectionManager);
		private var _eventBusRef:SharedReference=SharedReference.newReference(EventBus);
		private var _proxyRegistryRef:SharedReference=SharedReference.newReference(ProxyRegistry);

		/**
		 * A global instance of this class. This is the instance that would typically be used
		 * by an application.
		 *
		 * @return
		 *    An instance of CommandRegistry for global use.
		 */
		public static function get instance():CommandRegistry
		{
			if (_instance == null)
			{
				_instance=new CommandRegistry(EventBus.instance, ProxyRegistry.instance);
			}
			return _instance;
		}

		/**
		 * Initializes a new instance of this class.
		 *
		 * @param eventBus
		 *    The event bus to associate with the new command registry.
		 *
		 * @param proxyRegistry
		 *    The proxy registry to associate with the new command registry.
		 */
		public function CommandRegistry(eventBus:EventBus=null, proxyRegistry:ProxyRegistry=null)
		{
			if (eventBus == null)
			{
				eventBus=new EventBus();
			}
			setEventBus(eventBus);
			if (proxyRegistry == null)
			{
				proxyRegistry=new ProxyRegistry(this.eventBus);
			}
			setProxyRegistry(proxyRegistry);
			this.injectionMgr=this.proxyRegistry.injectionMgr;
		}

		//------------------------------------------------------------------------------------
		// Instance properties
		// The property provider used avoids directly referencing the global instance.
		// This is done to simplify the object graph for Memory Profiler.

		//-------------------------------------------------------
		// Property: eventBus

		/**
		 * The EventBus associated with this registry. This registry would respond to the
		 * request events on this bus by creating commands and posting back their responses.
		 *
		 * @return
		 *    The associated event bus.
		 */
		public function get eventBus():EventBus
		{
			return _eventBusRef.object;
		}

		/**
		 * @private
		 */
		private function setEventBus(value:EventBus):void
		{
			_eventBusRef.object=value;
		}


		//-------------------------------------------------------
		// Property: proxyRegistry

		/**
		 * The proxy registry associated with this command registry. This proxy registry would
		 * be used to inject proxies into the commands created by this command registry.
		 *
		 * @return
		 *    The associated proxy registry.
		 */
		public function get proxyRegistry():ProxyRegistry
		{
			return _proxyRegistryRef.object;
		}


		/**
		 * @private
		 */
		private function setProxyRegistry(value:ProxyRegistry):void
		{
			_proxyRegistryRef.object=value;
		}

		//-------------------------------------------------------
		// Property: injectionManager

		private function get injectionMgr():InjectionManager
		{
			return _injectionMgrRef.object;
		}

		private function set injectionMgr(value:InjectionManager):void
		{
			_injectionMgrRef.object=value;
		}


		/**
		 * Adds a CommandSet instance to this registry. A CommandSet contains references to a
		 * bunch of command classes in the app.
		 *
		 * @param cmdSet
		 *    The command set to be added.
		 */
		public final function addCommandSet(cmdSet:CommandSet):void
		{
			if (cmdSet == null)
			{
				return;
			}
			// Avoiding reflection (describeType) on the command class for performance
			// concerns.
			for each (var mapping:CommandMapping in cmdSet.mappings)
			{
				addCommand(mapping.command, mapping.request);
			}
			// We don't know the request IDs for a straight list of Command classes, so need
			// reflection here.
			for each (var cmdClass:Class in cmdSet.commands)
			{
				var requestHandlerCount:uint=registerAllCmdsProvidedBy(cmdClass);
				if (requestHandlerCount == 0)
				{
					Diagnostics.assert(false, "No [RequestHandler]s found in: " + cmdClass);
				}
			}
			if (cmdSet.registry == null)
			{
				cmdSet.registry=this;
			}
		}

		/**
		 * Adds command mappings for the given command class. If a request id is provided, the
		 * command is assumed to handle a single request type. Otherwise, the [RequestHandler]
		 * methods in the class are looked up for the applicable request IDs.
		 *
		 * @param commandClass
		 *    A command class that handles one or more request types.
		 *
		 * @param requestEventId
		 *    The request event id to add the command for, so that an event of this type on
		 *    the event bus would translate to the given command.
		 */
		public final function addCommand(commandClass:Class, requestEventId:String=null):void
		{
			if (requestEventId != null)
			{
				registerCmdForRequest(commandClass, requestEventId);
			}
			else
			{
				registerAllCmdsProvidedBy(commandClass);
			}
		}

		/**
		 * Removes the association of the given request event id with the given command class.
		 * If no command class is specified, all commands registered for the given event id
		 * are removed.
		 *
		 * @param requestEventId
		 *    The request event id whose commands are to be removed.
		 *
		 * @param commandClass
		 *    The command that should be removed for the given request id.
		 */
		public final function removeCommand(requestEventId:String, commandClass:Class=null):void
		{
			var registeredCmd:Object=_mappings[requestEventId];
			if (registeredCmd == null)
			{
				return;
			}
			if (commandClass == null || registeredCmd == commandClass)
			{
				// unregister from the bus for the given event
				this.eventBus.removeEventListener(requestEventId, onCommandRequest);
				delete _mappings[requestEventId];
				return;
			}
			if (registeredCmd is Set)
			{
				var cmdSet:Set=Set(registeredCmd);
				cmdSet.remove(commandClass);
				if (cmdSet.length == 0)
				{
					delete _mappings[requestEventId];
				}
				return;
			}
		}

		/**
		 * Creates a new instance of the given class and initializes it by setting certain
		 * properties, such as those tagged as [InjectableProxy].
		 *
		 * @param commandClass
		 *    The type whose instance is desired.
		 *
		 * @return
		 *    A new command instance of the given type.
		 */
		public final function newCommand(commandClass:Class):Object
		{
			return createAndSetupCommand(commandClass);
		}

		private function registerCmdForRequest(commandClass:Class, requestEventId:String):void
		{
			var currentlyRegisteredCmd:Object=_mappings[requestEventId];
			if (commandClass == currentlyRegisteredCmd)
			{
				// duplicate registrations of the same command are ignored
				return;
			}
			if (currentlyRegisteredCmd is Set)
			{
				Set(currentlyRegisteredCmd).add(commandClass);
				return;
			}
			if (currentlyRegisteredCmd)
			{
				var newCmdSet:Set=new Set();
				newCmdSet.add(currentlyRegisteredCmd);
				newCmdSet.add(commandClass);
				_mappings[requestEventId]=newCmdSet;
				return;
			}
			// first time registration for this command request id
			_mappings[requestEventId]=commandClass;
			// register with the bus for the given event
			this.eventBus.addEventListener(requestEventId, onCommandRequest);
		}

		/**
		 * @return
		 *    The number of request IDs that the command is registered for.
		 */
		private function registerAllCmdsProvidedBy(commandClass:Class):uint
		{
			var requestIDs:Array=ReflectionUtil.getMethodTagValues(commandClass, "RequestHandler");
			for each (var requestId:String in requestIDs)
			{
				registerCmdForRequest(commandClass, requestId);
			}
			return requestIDs.length;
		}

		private function onCommandRequest(request:Event):void
		{
			var command:Object=_mappings[request.type];
			if (command is Set)
			{
				Set(command).forEach(execute, request);
			}
			else if (command is Class)
			{
				execute(Class(command), request);
			}
		}

		/**
		 * All command executions (regardless of whether the command is an object or a static
		 * method) must go through this helper method.
		 */
		private function execute(commandClass:Class, request:Event):void
		{
			if (ReflectionUtil.hasMethod(commandClass, CONVENTIONAL_CMD_METHOD_NAME))
			{
				if (commandClass.hasOwnProperty(CONVENTIONAL_CMD_METHOD_NAME))
				{
					// class or static method
					commandClass[CONVENTIONAL_CMD_METHOD_NAME]();
				}
				else
				{
					// instance method
					var command:Object=createAndSetupCommand(commandClass, request);
					command[CONVENTIONAL_CMD_METHOD_NAME]();
				}
				return;
			}
			// There's no "execute" method in this command; invoke the explicitly tagged
			// request/event handler, if any.
			var evHandlerName:String=ReflectionUtil.getSingleTaggedMethod(commandClass, "RequestHandler", request.type);
			invokeRequestHandler(commandClass, evHandlerName, request);
		}

		private function invokeRequestHandler(cmdClass:Class, methodName:String, request:Event):void
		{
			var target:Object;
			if (cmdClass.hasOwnProperty(methodName))
			{
				target=cmdClass;
			}
			else
			{
				target=createAndSetupCommand(cmdClass, request);
				if (!target.hasOwnProperty(methodName))
				{
					return;
				}
			}
			var argTypes:Array /*Class*/=ReflectionUtil.getArgTypes(cmdClass, target, methodName);
			// The 1st arg, if any, is always supposed to be the original request event; so we
			// validate that first.
			if (argTypes.length > 0 && !(request is argTypes[0]))
			{
				// Note that this is not supposed to be an assertion or alert. Two different
				// modules could be using the same event id but different event classes. This
				// is a legit case, and should not cause an error. But an informational message
				// could be helpful.
				var msg:String=StringUtil.substitute("CmdRegistry: skipped invocation of {0}.{1} due to type incompatibility", cmdClass, methodName);
				trace(msg);
				return;
			}
			// Perform invocation based on the number of arguments.

			if (argTypes.length == 0)
			{
				// a no-arg method
				target[methodName]();
				return;
			}
			if (argTypes.length == 1)
			{
				// 1-arg request handlers are supposed to take the request event as their only
				// arg.
				target[methodName](request);
				return;
			}
			invokeMultiArgRequestHandler(target, methodName, argTypes, request);
		}

		/**
		 * Handles multi-arg command methods. 1st arg is always the request event; last arg
		 * can optionally be a callback; and there can be any number of args in b/w, which are
		 * looked up by their types from the ProxyRegistry.
		 */
		private function invokeMultiArgRequestHandler(target:Object, methodName:String, argTypes:Array /*Class*/, request:Event):void
		{
			var args:Array /*Object*/=new Array(argTypes.length);
			args[0]=request;
			var proxyArgStartIndex:int=1;
			var proxyArgEndIndex:int=argTypes.length - 1;
			if (argTypes[proxyArgEndIndex] == Function)
			{
				// the last argument to this request handler is a callback
				args[proxyArgEndIndex]=function(returnEv:MethodReturnEvent):void
				{
					CallbackUtil.dispatchResponse(request, returnEv);
				}
				proxyArgEndIndex-=1;
			}
			for (var i:int=proxyArgStartIndex; i <= proxyArgEndIndex; i++)
			{
				args[i]=this.proxyRegistry.getProxyByType(argTypes[i]);
			}
			var cmdMethod:Function=target[methodName];
			// invoke the request handler
			cmdMethod.apply(target, args);
		}

		private function createAndSetupCommand(commandClass:Class, request:Event=null):Object
		{
			var command:Object=new commandClass;
			// set the proxies on the command
			this.proxyRegistry.setInjectableProxiesOn(command);
			// get the [Injectable] properties processed
			this.injectionMgr.injectTargetsUsingProviders([command], undefined);

			if (request != null)
			{
				// Set the [InjectableRequest] (or equivalently, the [Request]) property.
				ReflectionUtil.setTaggedPropertyValue(command, "InjectableRequest|Request", request);
			}
			// weak-register with the event bus
			this.eventBus.addParticipant(command, true);
			return command;
		}
	} // class

}